// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/android/java/gin_java_bridge_dispatcher_host.h"

#include "base/android/jni_android.h"
#include "base/android/scoped_java_ref.h"
#include "base/memory/ptr_util.h"
#include "build/build_config.h"
#include "content/browser/android/java/gin_java_bound_object_delegate.h"
#include "content/browser/android/java/gin_java_bridge_message_filter.h"
#include "content/browser/android/java/java_bridge_thread.h"
#include "content/browser/android/java/jni_helper.h"
#include "content/common/android/gin_java_bridge_value.h"
#include "content/common/android/hash_set.h"
#include "content/common/gin_java_bridge_messages.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"

#if !defined(OS_ANDROID)
#error "JavaBridge only supports OS_ANDROID"
#endif

namespace content {

GinJavaBridgeDispatcherHost::GinJavaBridgeDispatcherHost(
    WebContents* web_contents,
    const base::android::JavaRef<jobject>& retained_object_set)
    : WebContentsObserver(web_contents)
    , next_object_id_(1)
    , retained_object_set_(base::android::AttachCurrentThread(),
          retained_object_set)
    , allow_object_contents_inspection_(true)
{
    DCHECK(!retained_object_set.is_null());
}

GinJavaBridgeDispatcherHost::~GinJavaBridgeDispatcherHost()
{
}

// GinJavaBridgeDispatcherHost gets created earlier than RenderProcessHost
// is initialized. So we postpone installing the message filter until we know
// that the RPH is in a good shape. Also, message filter installation is
// postponed until the first named object is created.
void GinJavaBridgeDispatcherHost::InstallFilterAndRegisterAllRoutingIds()
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (named_objects_.empty() || !web_contents()->GetRenderProcessHost()->GetChannel()) {
        return;
    }

    auto filter = GinJavaBridgeMessageFilter::FromHost(this, true);
    // ForEachFrame is synchronous.
    web_contents()->ForEachFrame(
        base::Bind(&GinJavaBridgeMessageFilter::AddRoutingIdForHost, filter,
            base::Unretained(this)));
}

void GinJavaBridgeDispatcherHost::RenderFrameCreated(
    RenderFrameHost* render_frame_host)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (auto filter = GinJavaBridgeMessageFilter::FromHost(this, false)) {
        filter->AddRoutingIdForHost(this, render_frame_host);
    } else {
        InstallFilterAndRegisterAllRoutingIds();
    }
    for (NamedObjectMap::const_iterator iter = named_objects_.begin();
         iter != named_objects_.end();
         ++iter) {
        render_frame_host->Send(new GinJavaBridgeMsg_AddNamedObject(
            render_frame_host->GetRoutingID(), iter->first, iter->second));
    }
}

void GinJavaBridgeDispatcherHost::WebContentsDestroyed()
{
    scoped_refptr<GinJavaBridgeMessageFilter> filter = GinJavaBridgeMessageFilter::FromHost(this, false);
    if (filter)
        filter->RemoveHost(this);
}

void GinJavaBridgeDispatcherHost::RenderProcessGone(
    base::TerminationStatus status)
{
    GinJavaBridgeMessageFilter::RemoveFilter(this);
}

void GinJavaBridgeDispatcherHost::RenderViewHostChanged(
    RenderViewHost* old_host,
    RenderViewHost* new_host)
{
    scoped_refptr<GinJavaBridgeMessageFilter> filter = GinJavaBridgeMessageFilter::FromHost(this, false);
    if (!filter)
        InstallFilterAndRegisterAllRoutingIds();
}

GinJavaBoundObject::ObjectID GinJavaBridgeDispatcherHost::AddObject(
    const base::android::JavaRef<jobject>& object,
    const base::android::JavaRef<jclass>& safe_annotation_clazz,
    bool is_named,
    int32_t holder)
{
    // Can be called on any thread. Calls come from the UI thread via
    // AddNamedObject, and from the background thread, when injected Java
    // object's method returns a Java object.
    DCHECK(is_named || holder);
    JNIEnv* env = base::android::AttachCurrentThread();
    JavaObjectWeakGlobalRef ref(env, object.obj());
    scoped_refptr<GinJavaBoundObject> new_object = is_named ? GinJavaBoundObject::CreateNamed(ref, safe_annotation_clazz)
                                                            : GinJavaBoundObject::CreateTransient(ref, safe_annotation_clazz,
                                                                holder);
    GinJavaBoundObject::ObjectID object_id;
    {
        base::AutoLock locker(objects_lock_);
        object_id = next_object_id_++;
        objects_[object_id] = new_object;
    }
#if DCHECK_IS_ON()
    {
        GinJavaBoundObject::ObjectID added_object_id;
        DCHECK(FindObjectId(object, &added_object_id));
        DCHECK_EQ(object_id, added_object_id);
    }
#endif // DCHECK_IS_ON()
    base::android::ScopedJavaLocalRef<jobject> retained_object_set = retained_object_set_.get(env);
    if (!retained_object_set.is_null()) {
        base::AutoLock locker(objects_lock_);
        JNI_Java_HashSet_add(env, retained_object_set, object);
    }
    return object_id;
}

bool GinJavaBridgeDispatcherHost::FindObjectId(
    const base::android::JavaRef<jobject>& object,
    GinJavaBoundObject::ObjectID* object_id)
{
    // Can be called on any thread.
    JNIEnv* env = base::android::AttachCurrentThread();
    base::AutoLock locker(objects_lock_);
    for (const auto& pair : objects_) {
        if (env->IsSameObject(
                object.obj(),
                pair.second->GetLocalRef(env).obj())) {
            *object_id = pair.first;
            return true;
        }
    }
    return false;
}

JavaObjectWeakGlobalRef GinJavaBridgeDispatcherHost::GetObjectWeakRef(
    GinJavaBoundObject::ObjectID object_id)
{
    scoped_refptr<GinJavaBoundObject> object = FindObject(object_id);
    if (object.get())
        return object->GetWeakRef();
    else
        return JavaObjectWeakGlobalRef();
}

JavaObjectWeakGlobalRef
GinJavaBridgeDispatcherHost::RemoveHolderAndAdvanceLocked(
    int32_t holder,
    ObjectMap::iterator* iter_ptr)
{
    objects_lock_.AssertAcquired();
    JavaObjectWeakGlobalRef result;
    scoped_refptr<GinJavaBoundObject> object((*iter_ptr)->second);
    bool object_erased = false;
    if (!object->IsNamed()) {
        object->RemoveHolder(holder);
        if (!object->HasHolders()) {
            result = object->GetWeakRef();
            objects_.erase((*iter_ptr)++);
            object_erased = true;
        }
    }
    if (!object_erased) {
        ++(*iter_ptr);
    }
    return result;
}

void GinJavaBridgeDispatcherHost::RemoveFromRetainedObjectSetLocked(
    const JavaObjectWeakGlobalRef& ref)
{
    objects_lock_.AssertAcquired();
    JNIEnv* env = base::android::AttachCurrentThread();
    base::android::ScopedJavaLocalRef<jobject> retained_object_set = retained_object_set_.get(env);
    if (!retained_object_set.is_null()) {
        JNI_Java_HashSet_remove(env, retained_object_set, ref.get(env));
    }
}

void GinJavaBridgeDispatcherHost::AddNamedObject(
    const std::string& name,
    const base::android::JavaRef<jobject>& object,
    const base::android::JavaRef<jclass>& safe_annotation_clazz)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    GinJavaBoundObject::ObjectID object_id;
    NamedObjectMap::iterator iter = named_objects_.find(name);
    bool existing_object = FindObjectId(object, &object_id);
    if (existing_object && iter != named_objects_.end() && iter->second == object_id) {
        // Nothing to do.
        return;
    }
    if (iter != named_objects_.end()) {
        RemoveNamedObject(iter->first);
    }
    if (existing_object) {
        base::AutoLock locker(objects_lock_);
        objects_[object_id]->AddName();
    } else {
        object_id = AddObject(object, safe_annotation_clazz, true, 0);
    }
    named_objects_[name] = object_id;

    // As GinJavaBridgeDispatcherHost can be created later than WebContents has
    // notified the observers about new RenderFrame, it is necessary to ensure
    // here that all render frame IDs are registered with the filter.
    InstallFilterAndRegisterAllRoutingIds();
    web_contents()->SendToAllFrames(
        new GinJavaBridgeMsg_AddNamedObject(MSG_ROUTING_NONE, name, object_id));
}

void GinJavaBridgeDispatcherHost::RemoveNamedObject(
    const std::string& name)
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    NamedObjectMap::iterator iter = named_objects_.find(name);
    if (iter == named_objects_.end())
        return;

    // |name| may come from |named_objects_|. Make a copy of name so that if
    // |name| is from |named_objects_| it'll be valid after the remove below.
    const std::string copied_name(name);

    {
        base::AutoLock locker(objects_lock_);
        objects_[iter->second]->RemoveName();
    }
    named_objects_.erase(iter);

    // As the object isn't going to be removed from the JavaScript side until the
    // next page reload, calls to it must still work, thus we should continue to
    // hold it. All the transient objects and removed named objects will be purged
    // during the cleansing caused by DocumentAvailableInMainFrame event.

    web_contents()->SendToAllFrames(
        new GinJavaBridgeMsg_RemoveNamedObject(MSG_ROUTING_NONE, copied_name));
}

void GinJavaBridgeDispatcherHost::SetAllowObjectContentsInspection(bool allow)
{
    if (!JavaBridgeThread::CurrentlyOn()) {
        JavaBridgeThread::GetTaskRunner()->PostTask(
            FROM_HERE,
            base::Bind(
                &GinJavaBridgeDispatcherHost::SetAllowObjectContentsInspection,
                this, allow));
        return;
    }
    allow_object_contents_inspection_ = allow;
}

void GinJavaBridgeDispatcherHost::DocumentAvailableInMainFrame()
{
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    // Called when the window object has been cleared in the main frame.
    // That means, all sub-frames have also been cleared, so only named
    // objects survived.
    JNIEnv* env = base::android::AttachCurrentThread();
    base::android::ScopedJavaLocalRef<jobject> retained_object_set = retained_object_set_.get(env);
    base::AutoLock locker(objects_lock_);
    if (!retained_object_set.is_null()) {
        JNI_Java_HashSet_clear(env, retained_object_set);
    }
    auto iter = objects_.begin();
    while (iter != objects_.end()) {
        if (iter->second->IsNamed()) {
            if (!retained_object_set.is_null()) {
                JNI_Java_HashSet_add(
                    env, retained_object_set, iter->second->GetLocalRef(env));
            }
            ++iter;
        } else {
            objects_.erase(iter++);
        }
    }
}

scoped_refptr<GinJavaBoundObject> GinJavaBridgeDispatcherHost::FindObject(
    GinJavaBoundObject::ObjectID object_id)
{
    // Can be called on any thread.
    base::AutoLock locker(objects_lock_);
    auto iter = objects_.find(object_id);
    if (iter != objects_.end())
        return iter->second;
    LOG(ERROR) << "WebView: Unknown object: " << object_id;
    return nullptr;
}

void GinJavaBridgeDispatcherHost::OnGetMethods(
    GinJavaBoundObject::ObjectID object_id,
    std::set<std::string>* returned_method_names)
{
    DCHECK(JavaBridgeThread::CurrentlyOn());
    if (!allow_object_contents_inspection_)
        return;
    scoped_refptr<GinJavaBoundObject> object = FindObject(object_id);
    if (object.get())
        *returned_method_names = object->GetMethodNames();
}

void GinJavaBridgeDispatcherHost::OnHasMethod(
    GinJavaBoundObject::ObjectID object_id,
    const std::string& method_name,
    bool* result)
{
    DCHECK(JavaBridgeThread::CurrentlyOn());
    scoped_refptr<GinJavaBoundObject> object = FindObject(object_id);
    if (object.get())
        *result = object->HasMethod(method_name);
}

void GinJavaBridgeDispatcherHost::OnInvokeMethod(
    int routing_id,
    GinJavaBoundObject::ObjectID object_id,
    const std::string& method_name,
    const base::ListValue& arguments,
    base::ListValue* wrapped_result,
    content::GinJavaBridgeError* error_code)
{
    DCHECK(JavaBridgeThread::CurrentlyOn());
    DCHECK(routing_id != MSG_ROUTING_NONE);
    scoped_refptr<GinJavaBoundObject> object = FindObject(object_id);
    if (!object.get()) {
        wrapped_result->Append(base::Value::CreateNullValue());
        *error_code = kGinJavaBridgeUnknownObjectId;
        return;
    }
    scoped_refptr<GinJavaMethodInvocationHelper> result = new GinJavaMethodInvocationHelper(
        base::MakeUnique<GinJavaBoundObjectDelegate>(object), method_name,
        arguments);
    result->Init(this);
    result->Invoke();
    *error_code = result->GetInvocationError();
    if (result->HoldsPrimitiveResult()) {
        std::unique_ptr<base::ListValue> result_copy(
            result->GetPrimitiveResult().DeepCopy());
        wrapped_result->Swap(result_copy.get());
    } else if (!result->GetObjectResult().is_null()) {
        GinJavaBoundObject::ObjectID returned_object_id;
        if (FindObjectId(result->GetObjectResult(), &returned_object_id)) {
            base::AutoLock locker(objects_lock_);
            objects_[returned_object_id]->AddHolder(routing_id);
        } else {
            returned_object_id = AddObject(result->GetObjectResult(),
                result->GetSafeAnnotationClass(),
                false,
                routing_id);
        }
        wrapped_result->Append(
            GinJavaBridgeValue::CreateObjectIDValue(
                returned_object_id)
                .release());
    } else {
        wrapped_result->Append(base::Value::CreateNullValue());
    }
}

void GinJavaBridgeDispatcherHost::OnObjectWrapperDeleted(
    int routing_id,
    GinJavaBoundObject::ObjectID object_id)
{
    DCHECK(JavaBridgeThread::CurrentlyOn());
    DCHECK(routing_id != MSG_ROUTING_NONE);
    base::AutoLock locker(objects_lock_);
    auto iter = objects_.find(object_id);
    if (iter == objects_.end())
        return;
    JavaObjectWeakGlobalRef ref = RemoveHolderAndAdvanceLocked(routing_id, &iter);
    if (!ref.is_uninitialized()) {
        RemoveFromRetainedObjectSetLocked(ref);
    }
}

} // namespace content
